#region Usings

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Castle.ActiveRecord;
using Castle.ActiveRecord.Queries;
using Castle.Components.Validator;
using log4net;
using NHibernate;
using Karma.Framework.Core.Domain.Tools.Import.Layout;

#endregion

namespace Karma.Framework.Core.Domain.Tools.Import.Validation
{
    public class DefaultUniqueKeyValidator : IUniqueKeyValidator
    {
        private static readonly ILog LOG = LogManager.GetLogger(typeof (DefaultUniqueKeyValidator).Name);

        #region IUniqueKeyValidator Members

        public ValidationSummary Validate<T>(EntityLayout layout, IList<T> entities) where T : DomainObject
        {
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug(string.Format("Validating UniqueKeys for entity {0}", layout.Entity.FullName));
            }
            var summary = new ValidationSummary();

            var session = ActiveRecordMediator.GetSessionFactoryHolder ().CreateSession (typeof (T));
            var errors = ValidateEntity(layout, entities, session);
            if (layout.Aggregates != null)
            {
                foreach (AggregateLayout aggregateLayout in layout.Aggregates)
                {
                    var property = (from p in layout.Entity.GetProperties()
                                    where p.Name.Equals(aggregateLayout.Name)
                                    select p).Single();

                    var aggregates = (from e in entities
                                      select property.GetValue(e, new object[] { })).ToList();

                    var aggErrors = ValidateEntity(aggregateLayout, aggregates, session);
                    if (aggErrors != null)
                    {
                        errors.Errors = errors.Errors.Concat(aggErrors.Errors).ToList();
                    }
                }
                if (errors != null)
                {
                    summary.ErrorSummary = errors;
                }
            }
            ActiveRecordMediator.GetSessionFactoryHolder().ReleaseSession(session);
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug("UniqueKeys validated.");
            }
            return summary;
        }

        #endregion

        private ErrorSummary ValidateEntity<T>(EntityLayout layout, IList<T> entities, ISession session)
        {
            ErrorSummary summary = null;

            // preprocess properties
            var properties = (from p in layout.Properties
                             where !string.IsNullOrEmpty(p.UniqueKey) || p.Unique
                             orderby p.Name
                             select new PropertyData { Field = p, PropertyInfo = typeof(T).GetProperty(p.Name), Index = layout.Properties.IndexOf (p) + 1 });
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug ("Properties to validate");
                foreach (var property in properties)
                {
                    LOG.Debug (string.Format ("Property Name: {0}, UniqueKey: {1}", property.Field.Name, property.Field.UniqueKey));
                }
            }
            var groups = (from p in properties
                          orderby p.Field.Name
                          group p by p.Field.UniqueKey ?? p.Field.Name
                          into g
                              select g);
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug("The groups for the constraints");
                foreach (var group in groups)
                {
                    LOG.Debug (string.Format ("Group name: {0}", group.Key));
                    foreach (var property in group)
                    {
                        LOG.Debug(string.Format("Property Name: {0}, UniqueKey: {1}", property.Field.Name, property.Field.UniqueKey));
                    }
                }
            }
            if (properties.Count() > 0 && groups.Count() > 0)
            {
                // build queries
                var queries = new List<PropertyGroupData>();
                foreach (var _group in groups)
                {
                    var query = new StringBuilder(string.Format(
                        "select count(distinct e.Id) from {0} e where ", typeof(T).Name));
                    var groupArray = _group.ToArray();

                    for (var i = 0; i < groupArray.Length; i++)
                    {
                        var name = groupArray[i].Field.Name;
                        if (groupArray[i].Field is ReferenceField)
                        {
                            var refFld = (ReferenceField)groupArray[i].Field;
                            var refPropName = refFld.KeyRef ?? "Id";
                            query.Append(" e.").Append(name).Append(".").Append(refPropName).Append("=:").Append(name);
                        }
                        else
                        {
                            query.Append(" e.").Append(name).Append("=:").Append(name);
                        }
                        if (i < groupArray.Length - 1)
                        {
                            query.Append(" and ");
                        }
                    }
                    if (LOG.IsDebugEnabled)
                    {
                        LOG.Debug(string.Format("The filter used for group {0}: {1}", _group.Key, query));
                    }

                    queries.Add(
                        new PropertyGroupData { Group = _group, Query = query.ToString() });
                }

                // do validations
                var idx = 0;
                foreach (var entity in entities)
                {
                    if (LOG.IsDebugEnabled && idx % 200 == 0)
                    {
                        LOG.Debug(string.Format("{0} records validated.", idx));
                    }
                    var multiQuery = new ActiveRecordMultiQuery(typeof(T));
                    var values = new ArrayList();
                    foreach (var ukQuery in queries)
                    {
                        // build the query
                        var arquery = new ScalarQuery<Int64>(typeof(T), QueryLanguage.Hql, ukQuery.Query);
                        var propertyValues = (from p in ukQuery.Group
                                              orderby p.Field.Name
                                              select new
                                              {
                                                  p.Field.Name,
                                                  Value = p.PropertyInfo.GetValue(entity, new object[] { }),
                                                  p.Field
                                              }).ToArray();
                        // set param values
                        foreach (var propertyValue in propertyValues)
                        {
                            if (propertyValue.Field is ReferenceField)
                            {
                                var refFld = (ReferenceField) propertyValue.Field;
                                var refPropName = refFld.KeyRef ?? "Id";
                                var refProperty = refFld.FieldType.GetProperty(refPropName);

                                if (propertyValue.Value != null)
                                {
                                    var referenceValue = refProperty.GetValue(propertyValue.Value, new object[] { });
                                    arquery.SetParameter(propertyValue.Name, referenceValue);
                                }
                            }
                            else
                            {
                                arquery.SetParameter(propertyValue.Name, propertyValue.Value);
                            }
                        }
                        multiQuery.Add(arquery);
                        var pvalues = propertyValues.Select(x => x.Value).ToArray();
                        if (LOG.IsDebugEnabled)
                        {
                            foreach (var value in propertyValues)
                            {
                                LOG.Debug(string.Format("Name [{0}], Value [{1}].", value.Name, value.Value));
                            }
                        }
                        values.Add(pvalues);
                    }
                    // execute and inspect results
                    var results = (IList)multiQuery.Execute(session);
                    for (var i = 0; i < results.Count; i++)
                    {
                        var result = (Int64)((IList)results[i])[0];
                        if (result > 0)
                        {
                            var groupData = queries[i];
                            AddErrorToSummary(ref summary, idx + 1, groupData, (object[])values[i]);
                        }
                    }
                    idx++;
                }
            }
            return summary;
        }

        private class PropertyGroupData
        {
            public IGrouping<string, PropertyData> Group { get; set; }
            public string Query { get; set; }
        }

        private class PropertyData
        {
            public Property Field { get; set; }
            public int Index { get; set; }
            public PropertyInfo PropertyInfo { get; set; }
        }

        private static void AddErrorToSummary(ref ErrorSummary summary, int row, PropertyGroupData group, object []values)
        {
            if (summary == null)
            {
                summary = new ErrorSummary { Errors = new List <ErrorDescription> () };
            }
            var propNames = group.Group.Select(x => x.Field.Name).ToArray();
            var propsb = new StringBuilder();
            for (var i = 0; i < propNames.Length; i++)
            {
                propsb.Append(propNames[i]);
                if (i < propNames.Length - 1)
                {
                    propsb.Append(",");
                }
            }
            var valsb = new StringBuilder();
            for (var i = 0; i < values.Length; i++)
            {
                valsb.Append(values[i]);
                if (i < values.Length - 1)
                {
                    valsb.Append(",");
                }
            }
            if (LOG.IsDebugEnabled)
            {
                LOG.Debug(string.Format(
                                           "[[Error]] de integridad de datos, [{0}] = [{1}] ya se encuentran registradas",
                                           propsb, valsb));
            }
            foreach (var propertyData in group.Group)
            {
                summary.Errors.Add(new ErrorDescription
                {
                    Line = row,
                    Column = propertyData.Index,
                    Property = propertyData.Field,
                    Severity = ErrorSeverity.Error,
                    Type = ErrorType.Data,
                    Description =
                        string.Format(
                        "Error de integridad de datos, [{0}] = [{1}] ya se encuentran registradas",
                        propsb, valsb)
                });
            }
        }
    
    }
}